@@ -48,12 +48,12 @@ platforms :ruby_18 do |
||
48 | 48 |
end |
49 | 49 |
|
50 | 50 |
group :development do |
51 |
- gem 'pry' |
|
52 | 51 |
gem 'binding_of_caller' |
53 | 52 |
gem 'better_errors' |
54 | 53 |
end |
55 | 54 |
|
56 | 55 |
group :development, :test do |
56 |
+ gem 'pry' |
|
57 | 57 |
gem 'rspec-rails' |
58 | 58 |
gem 'rspec' |
59 | 59 |
gem 'shoulda-matchers' |
@@ -1,39 +1,39 @@ |
||
1 |
-# This controller is designed to allow your Agents to receive cross-site Webhooks (posts). When POSTed, your Agent will |
|
2 |
-# have #receive_webhook called on itself with the POST params. |
|
1 |
+# This controller is designed to allow your Agents to receive cross-site Webhooks (POSTs), or to output data streams. |
|
2 |
+# When a POST or GET is received, your Agent will have #receive_webhook called on itself with the incoming params. |
|
3 | 3 |
# |
4 |
-# Make POSTs to the following URL: |
|
4 |
+# To implement webhooks, make POSTs to the following URL: |
|
5 | 5 |
# http://yourserver.com/users/:user_id/webhooks/:agent_id/:secret |
6 | 6 |
# where :user_id is your User's id, :agent_id is an Agent's id, and :secret is a token that should be |
7 | 7 |
# user-specifiable in your Agent. It is highly recommended that you verify this token whenever #receive_webhook |
8 | 8 |
# is called. For example, one of your Agent's options could be :secret and you could compare this value |
9 | 9 |
# to params[:secret] whenever #receive_webhook is called on your Agent, rejecting invalid requests. |
10 | 10 |
# |
11 |
-# Your Agent's #receive_webhook method should return an Array of [json_or_string_response, status_code]. For example: |
|
11 |
+# Your Agent's #receive_webhook method should return an Array of [json_or_string_response, status_code, optional mime type]. For example: |
|
12 | 12 |
# [{status: "success"}, 200] |
13 | 13 |
# or |
14 |
-# ["not found", 404] |
|
14 |
+# ["not found", 404, 'text/plain'] |
|
15 | 15 |
|
16 | 16 |
class WebhooksController < ApplicationController |
17 | 17 |
skip_before_filter :authenticate_user! |
18 | 18 |
|
19 |
- def create |
|
19 |
+ def handle_request |
|
20 | 20 |
user = User.find_by_id(params[:user_id]) |
21 | 21 |
if user |
22 | 22 |
agent = user.agents.find_by_id(params[:agent_id]) |
23 | 23 |
if agent |
24 |
- response, status = agent.trigger_webhook(params.except(:action, :controller, :agent_id, :user_id)) |
|
25 |
- if response.is_a?(String) |
|
26 |
- render :text => response, :status => status || 200 |
|
27 |
- elsif response.is_a?(Hash) |
|
28 |
- render :json => response, :status => status || 200 |
|
24 |
+ content, status, content_type = agent.trigger_webhook(params.except(:action, :controller, :agent_id, :user_id, :format), request.method_symbol.to_s, request.format.to_s) |
|
25 |
+ if content.is_a?(String) |
|
26 |
+ render :text => content, :status => status || 200, :content_type => content_type || 'text/plain' |
|
27 |
+ elsif content.is_a?(Hash) |
|
28 |
+ render :json => content, :status => status || 200 |
|
29 | 29 |
else |
30 |
- head :ok |
|
30 |
+ head(status || 200) |
|
31 | 31 |
end |
32 | 32 |
else |
33 |
- render :text => "agent not found", :status => :not_found |
|
33 |
+ render :text => "agent not found", :status => 404 |
|
34 | 34 |
end |
35 | 35 |
else |
36 |
- render :text => "user not found", :status => :not_found |
|
36 |
+ render :text => "user not found", :status => 404 |
|
37 | 37 |
end |
38 | 38 |
end |
39 | 39 |
end |
@@ -73,7 +73,7 @@ class Agent < ActiveRecord::Base |
||
73 | 73 |
# Implement me in your subclass of Agent. |
74 | 74 |
end |
75 | 75 |
|
76 |
- def receive_webhook(params) |
|
76 |
+ def receive_webhook(params, method, format) |
|
77 | 77 |
# Implement me in your subclass of Agent. |
78 | 78 |
["not implemented", 404] |
79 | 79 |
end |
@@ -136,8 +136,8 @@ class Agent < ActiveRecord::Base |
||
136 | 136 |
message.gsub(/<([^>]+)>/) { Utils.value_at(payload, $1) || "??" } |
137 | 137 |
end |
138 | 138 |
|
139 |
- def trigger_webhook(params) |
|
140 |
- receive_webhook(params).tap do |
|
139 |
+ def trigger_webhook(params, method, format) |
|
140 |
+ receive_webhook(params, method, format).tap do |
|
141 | 141 |
self.last_webhook_at = Time.now |
142 | 142 |
save! |
143 | 143 |
end |
@@ -78,7 +78,7 @@ module Agents |
||
78 | 78 |
"#{server_url}/users/#{self.user.id}/webhooks/#{self.id}/#{secret}" |
79 | 79 |
end |
80 | 80 |
|
81 |
- def receive_webhook(params) |
|
81 |
+ def receive_webhook(params, method, format) |
|
82 | 82 |
if memory['pending_calls'].has_key? params['secret'] |
83 | 83 |
response = Twilio::TwiML::Response.new {|r| r.Say memory['pending_calls'][params['secret']], :voice => 'woman'} |
84 | 84 |
memory['pending_calls'].delete params['secret'] |
@@ -36,9 +36,9 @@ module Agents |
||
36 | 36 |
"payload_path" => "payload"} |
37 | 37 |
end |
38 | 38 |
|
39 |
- def receive_webhook(params) |
|
39 |
+ def receive_webhook(params, method, format) |
|
40 | 40 |
secret = params.delete('secret') |
41 |
- return ["Not Authorized", 401] unless secret == options['secret'] |
|
41 |
+ return ["Not Authorized", 401] unless secret == options['secret'] && method == "post" |
|
42 | 42 |
|
43 | 43 |
create_event(:payload => payload_for(params)) |
44 | 44 |
|
@@ -31,7 +31,7 @@ Huginn::Application.routes.draw do |
||
31 | 31 |
match "/worker_status" => "worker_status#show" |
32 | 32 |
|
33 | 33 |
post "/users/:user_id/update_location/:secret" => "user_location_updates#create" |
34 |
- post "/users/:user_id/webhooks/:agent_id/:secret" => "webhooks#create" |
|
34 |
+ match "/users/:user_id/webhooks/:agent_id/:secret" => "webhooks#handle_request" |
|
35 | 35 |
|
36 | 36 |
# match "/delayed_job" => DelayedJobWeb, :anchor => false |
37 | 37 |
devise_for :users, :sign_out_via => [ :post, :delete ] |
@@ -5,10 +5,12 @@ describe WebhooksController do |
||
5 | 5 |
cannot_receive_events! |
6 | 6 |
cannot_be_scheduled! |
7 | 7 |
|
8 |
- def receive_webhook(params) |
|
8 |
+ def receive_webhook(params, method, format) |
|
9 | 9 |
if params.delete(:secret) == options[:secret] |
10 | 10 |
memory[:webhook_values] = params |
11 |
- ["success", 200] |
|
11 |
+ memory[:webhook_format] = format |
|
12 |
+ memory[:webhook_method] = method |
|
13 |
+ ["success", 200, memory['content_type']] |
|
12 | 14 |
else |
13 | 15 |
["failure", 404] |
14 | 16 |
end |
@@ -24,31 +26,72 @@ describe WebhooksController do |
||
24 | 26 |
|
25 | 27 |
it "should not require login to trigger a webhook" do |
26 | 28 |
@agent.last_webhook_at.should be_nil |
27 |
- post :create, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5" |
|
29 |
+ post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5" |
|
28 | 30 |
@agent.reload.last_webhook_at.should be_within(2).of(Time.now) |
29 | 31 |
response.body.should == "success" |
30 | 32 |
response.should be_success |
31 | 33 |
end |
32 | 34 |
|
33 | 35 |
it "should call receive_webhook" do |
34 |
- post :create, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5" |
|
35 |
- @agent.reload.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" } |
|
36 |
+ post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5" |
|
37 |
+ @agent.reload |
|
38 |
+ @agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" } |
|
39 |
+ @agent.memory[:webhook_format].should == "text/html" |
|
40 |
+ @agent.memory[:webhook_method].should == "post" |
|
36 | 41 |
response.body.should == "success" |
42 |
+ response.headers['Content-Type'].should == 'text/plain; charset=utf-8' |
|
37 | 43 |
response.should be_success |
38 | 44 |
|
39 |
- post :create, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "not_my_secret", :no => "go" |
|
45 |
+ post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "not_my_secret", :no => "go" |
|
40 | 46 |
@agent.reload.memory[:webhook_values].should_not == { 'no' => "go" } |
41 | 47 |
response.body.should == "failure" |
42 | 48 |
response.should be_missing |
43 | 49 |
end |
44 | 50 |
|
51 |
+ it "should accept gets" do |
|
52 |
+ get :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5" |
|
53 |
+ @agent.reload |
|
54 |
+ @agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" } |
|
55 |
+ @agent.memory[:webhook_format].should == "text/html" |
|
56 |
+ @agent.memory[:webhook_method].should == "get" |
|
57 |
+ response.body.should == "success" |
|
58 |
+ response.should be_success |
|
59 |
+ end |
|
60 |
+ |
|
61 |
+ it "should pass through the received format" do |
|
62 |
+ get :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5", :format => :json |
|
63 |
+ @agent.reload |
|
64 |
+ @agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" } |
|
65 |
+ @agent.memory[:webhook_format].should == "application/json" |
|
66 |
+ @agent.memory[:webhook_method].should == "get" |
|
67 |
+ |
|
68 |
+ post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5", :format => :xml |
|
69 |
+ @agent.reload |
|
70 |
+ @agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" } |
|
71 |
+ @agent.memory[:webhook_format].should == "application/xml" |
|
72 |
+ @agent.memory[:webhook_method].should == "post" |
|
73 |
+ |
|
74 |
+ put :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5", :format => :atom |
|
75 |
+ @agent.reload |
|
76 |
+ @agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" } |
|
77 |
+ @agent.memory[:webhook_format].should == "application/atom+xml" |
|
78 |
+ @agent.memory[:webhook_method].should == "put" |
|
79 |
+ end |
|
80 |
+ |
|
81 |
+ it "can accept a content-type to return" do |
|
82 |
+ @agent.memory['content_type'] = 'application/json' |
|
83 |
+ @agent.save! |
|
84 |
+ get :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5" |
|
85 |
+ response.headers['Content-Type'].should == 'application/json; charset=utf-8' |
|
86 |
+ end |
|
87 |
+ |
|
45 | 88 |
it "should fail on incorrect users" do |
46 |
- post :create, :user_id => users(:jane).to_param, :agent_id => @agent.id, :secret => "my_secret", :no => "go" |
|
89 |
+ post :handle_request, :user_id => users(:jane).to_param, :agent_id => @agent.id, :secret => "my_secret", :no => "go" |
|
47 | 90 |
response.should be_missing |
48 | 91 |
end |
49 | 92 |
|
50 | 93 |
it "should fail on incorrect agents" do |
51 |
- post :create, :user_id => users(:bob).to_param, :agent_id => 454545, :secret => "my_secret", :no => "go" |
|
94 |
+ post :handle_request, :user_id => users(:bob).to_param, :agent_id => 454545, :secret => "my_secret", :no => "go" |
|
52 | 95 |
response.should be_missing |
53 | 96 |
end |
54 | 97 |
end |
@@ -14,7 +14,7 @@ describe Agents::WebhookAgent do |
||
14 | 14 |
it 'should create event if secret matches' do |
15 | 15 |
out = nil |
16 | 16 |
lambda { |
17 |
- out = agent.receive_webhook('secret' => 'foobar', 'payload' => payload) |
|
17 |
+ out = agent.receive_webhook({ 'secret' => 'foobar', 'payload' => payload }, "post", "text/html") |
|
18 | 18 |
}.should change { Event.count }.by(1) |
19 | 19 |
out.should eq(['Event Created', 201]) |
20 | 20 |
Event.last.payload.should eq(payload) |
@@ -23,7 +23,15 @@ describe Agents::WebhookAgent do |
||
23 | 23 |
it 'should not create event if secrets dont match' do |
24 | 24 |
out = nil |
25 | 25 |
lambda { |
26 |
- out = agent.receive_webhook('secret' => 'bazbat', 'payload' => payload) |
|
26 |
+ out = agent.receive_webhook({ 'secret' => 'bazbat', 'payload' => payload }, "post", "text/html") |
|
27 |
+ }.should change { Event.count }.by(0) |
|
28 |
+ out.should eq(['Not Authorized', 401]) |
|
29 |
+ end |
|
30 |
+ |
|
31 |
+ it "should only accept POSTs" do |
|
32 |
+ out = nil |
|
33 |
+ lambda { |
|
34 |
+ out = agent.receive_webhook({ 'secret' => 'foobar', 'payload' => payload }, "get", "text/html") |
|
27 | 35 |
}.should change { Event.count }.by(0) |
28 | 36 |
out.should eq(['Not Authorized', 401]) |
29 | 37 |
end |
@@ -0,0 +1,19 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe "routing for webhooks" do |
|
4 |
+ it "routes to handle_request" do |
|
5 |
+ resulting_params = { :user_id => "6", :agent_id => "2", :secret => "foobar" } |
|
6 |
+ get("/users/6/webhooks/2/foobar").should route_to("webhooks#handle_request", resulting_params) |
|
7 |
+ post("/users/6/webhooks/2/foobar").should route_to("webhooks#handle_request", resulting_params) |
|
8 |
+ put("/users/6/webhooks/2/foobar").should route_to("webhooks#handle_request", resulting_params) |
|
9 |
+ delete("/users/6/webhooks/2/foobar").should route_to("webhooks#handle_request", resulting_params) |
|
10 |
+ end |
|
11 |
+ |
|
12 |
+ it "routes with format" do |
|
13 |
+ get("/users/6/webhooks/2/foobar.json").should route_to("webhooks#handle_request", |
|
14 |
+ { :user_id => "6", :agent_id => "2", :secret => "foobar", :format => "json" }) |
|
15 |
+ |
|
16 |
+ get("/users/6/webhooks/2/foobar.atom").should route_to("webhooks#handle_request", |
|
17 |
+ { :user_id => "6", :agent_id => "2", :secret => "foobar", :format => "atom" }) |
|
18 |
+ end |
|
19 |
+end |